/* vim: set ts=2 et sw=2 cindent fo=qroca: */
package com.globant.google.mendoza.malbec;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.cookie.CookiePolicy;
import org.apache.commons.httpclient.params.HttpClientParams;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.HttpWebConnection;
import com.gargoylesoftware.htmlunit.KeyValuePair;
import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.SubmitMethod;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.WebRequestSettings;
import com.globant.google.mendoza.malbec.schema._2.CheckoutRedirect;
import com.globant.google.mendoza.malbec.schema._2.CheckoutShoppingCart;
import com.thoughtworks.selenium.DefaultSelenium;
import com.thoughtworks.selenium.Selenium;
import com.thoughtworks.selenium.SeleniumException;

/** The simulated buyer.
 */
public class SeleniumBuyerRobot {

  /** The class logger.
   */
  private static Log log = LogFactory.getLog(SeleniumBuyerRobot.class);

  /** This is the timeout given by Google for merchant calculations to be
   * performed. The time is given in millis.
   */
  private static final long MC_TIMEOUT = 3001;

  /** Time to wait for bad login errors. The time is given in millis.
   */
  private static final long BAD_LOGIN_TIMEOUT = 1000;

    /** The url to post the cart to.  This url encodes the merchant id.
   */
  private String postURL;

  /** The firefox-bin path.
   */
  private String firefoxBinPath;

  /** The firefox path. The path given to selenium to start the browser.
   * Default value is "*chrome" + firefoxBinPath.
   */
  private String firefoxPath;

  /**
   * The cart encoder.
   */
  private CartPostEncoder encoder;

  /**
   * The merchant key used by the encoder to sign the cart.
   */
  private String merchantKey;

  /**
   * The base url for checkout.
   */
  private String serverUrl;

  /**
   * The buyer url for checkout.
   */
  private String buyerURL;

  /**
   * The hostname where selenium server is running.
   */
  private String seleniumHost;

  /**
   * The port used to connect to selenium server.
   */
  private int seleniumPort;

  /**
   * The selenium remote controller.
   */
  private DefaultSelenium seleniumRC;

  /**
   * Indicates if the seleniumRC is started or not.
   */
  private boolean seleniumRCStarted = false;

  /** Creates a simulated buyer.
   *
   * @param buyerUrl the buyerUrl. Cannot be null.
   *
   * @param url The GBuy url, including the merchant id. Cannot be null. Used
   * to post carts.
   *
   * @param theEncoder The encoder used to encode the cart.
   *
   * @param theMerchantKey The merchant key used by the encoder to sign the
   *  cart.
   *
   * @param theFirefoxBinPath The path to the firefox-bin file.
   *
   * @param seleniumServerHost the host where selenium server is running.
   *
   * @param seleniumServerPort the port used to connect to selenium server.
   */
  public SeleniumBuyerRobot(final String buyerUrl, final String url,
      final CartPostEncoder theEncoder, final String theMerchantKey,
      final String theFirefoxBinPath, final String seleniumServerHost,
      final int seleniumServerPort) {
    if (log.isTraceEnabled()) {
      log.trace("Entering Buyer('" + url + "')");
    }
    if (url == null) {
      throw new IllegalArgumentException("the url cannot be null");
    }
    if (theEncoder == null) {
      throw new IllegalArgumentException("the encoder cannot be null");
    }
    if (theMerchantKey == null) {
      throw new IllegalArgumentException("the merchant key cannot be null");
    }
    if (theFirefoxBinPath == null) {
      throw new IllegalArgumentException("the firefox-bin path cannot be null");
    }

    buyerURL = buyerUrl;
    postURL = url;
    encoder = theEncoder;
    merchantKey = theMerchantKey;
    firefoxBinPath = theFirefoxBinPath;
    firefoxPath = "*chrome " + firefoxBinPath;

    try {
      URL checkoutUrl = new URL(url);
      serverUrl = checkoutUrl.getProtocol() + "://"
      + checkoutUrl.getHost()
      + (checkoutUrl.getPort() == -1 ? "" : ":" + checkoutUrl.getPort());

      seleniumHost = seleniumServerHost;
      seleniumPort = seleniumServerPort;

      log.debug("Using firefox. Path: " + firefoxBinPath);
      log.debug("Connecting with selenium server at:" + seleniumHost + ":"
          + seleniumPort);
      seleniumRC = createSeleniumRc(serverUrl);
    } catch (MalformedURLException e) {
      throw new RuntimeException("The url received is malformed:" + url, e);
    }

    log.trace("Leaving Buyer");
  }

  /**
   * Create a new default selenium, which is the selenium rc.
   * @param theServerUrl The url where to star selenium interaction.
   * @return Return a brand new default selenium.
   */
  private DefaultSelenium createSeleniumRc(final String theServerUrl) {
    DefaultSelenium newRC =  new DefaultSelenium(
        seleniumHost, seleniumPort, firefoxPath, theServerUrl);
    return newRC;
  }

  /** Places an order to gbuy, specifying the cart as an JAXB cart.
  *
  * This function implements the buyer's actions necessary to place an order:
  *
  * 1- It posts the shopping cart to the gbuy url.
  *
  * 2- Logs in as a buyer.
  *
  * 3- If the initial address is specified, it posts it to the ajax url. This
  * triggers the merchant calculation callback for the predefined buyer
  * address.
  *
  * 4- It checks that the place order button is available.
  *
  * 5- It posts, one by one, the additional addresses or coupons/buy
  * certificates to the ajax url. This triggers the merchant calculation
  * callback in the server for the additional addresses, coupons and gift
  * certificates.
  *
  * 6- Places the order, posting the coupons and gift certificates found in
  * the additional addresses.
  *
  *
  * @param cart The checkout cart to send. It cannot be null.
  *
  * @param email The buyer email used to log in to the GBuy service as a
  * buyer. Cannot be null.
  *
  * @param password The passoword used to log in to the GBuy service as a
  * buyer. Cannot be null.
  * @return returns the order number of the new posted cart order.
  *
  */
 public String placeOrder(final CheckoutShoppingCart cart,
     final String email, final String password) {
   return placeOrder(cart, email, password, null);
 }

  /** Places an order to gbuy, specifying the cart as an JAXB cart.
   *
   * This function implements the buyer's actions necessary to place an order:
   *
   * 1- It posts the shopping cart to the gbuy url.
   *
   * 2- Logs in as a buyer.
   *
   * 3- If the initial address is specified, it posts it to the ajax url. This
   * triggers the merchant calculation callback for the predefined buyer
   * address.
   *
   * 4- It checks that the place order button is available.
   *
   * 5- It posts, one by one, the additional addresses or coupons/buy
   * certificates to the ajax url. This triggers the merchant calculation
   * callback in the server for the additional addresses, coupons and gift
   * certificates.
   *
   * 6- Places the order, posting the coupons and gift certificates found in
   * the additional addresses.
   *
   *
   * @param cart The checkout cart to send. It cannot be null.
   *
   * @param email The buyer email used to log in to the GBuy service as a
   * buyer. Cannot be null.
   *
   * @param password The passoword used to log in to the GBuy service as a
   * buyer. Cannot be null.
   *
   * @param buyerInformation A list of either shipping addresses, coupons or
   *  gift certificates.
   *
   * @return returns the order number of the new posted cart order.
   */
  public String placeOrder(final CheckoutShoppingCart cart,
      final String email, final String password,
      final List<BuyerInformation> buyerInformation) {
    return placeOrder(cart, email, password, buyerInformation, null);
  }

  /** Places an order to gbuy, specifying the cart as an JAXB cart.
  *
  * This function implements the buyer's actions necessary to place an order:
  *
  * 1- It posts the shopping cart to the gbuy url.
  *
  * 2- Logs in as a buyer.
  *
  * 3- If the initial address is specified, it posts it to the ajax url. This
  * triggers the merchant calculation callback for the predefined buyer
  * address.
  *
  * 4- It checks that the place order button is available.
  *
  * 5- It posts, one by one, the additional addresses or coupons/buy
  * certificates to the ajax url. This triggers the merchant calculation
  * callback in the server for the additional addresses, coupons and gift
  * certificates.
  *
  * 6- Places the order, posting the coupons and gift certificates found in
  * the additional addresses.
  *
  *
  * @param cart The checkout cart to send. It cannot be null.
  *
  * @param email The buyer email used to log in to the GBuy service as a
  * buyer. Cannot be null.
  *
  * @param password The passoword used to log in to the GBuy service as a
  * buyer. Cannot be null.
  *
  * @param buyerInformation A list of either shipping addresses, coupons or
  *  gift certificates.
  *
  * @param ccOptions the list of carrier calculated shipping options to verify.
  * @return returns the order number of the new posted cart order.
  */
 public String placeOrder(final CheckoutShoppingCart cart,
     final String email, final String password,
     final List<BuyerInformation> buyerInformation,
     final List<String> ccOptions) {

   log.trace("Entering placeOrder");

   if (cart == null) {
     throw new IllegalArgumentException("the cart cannot be null");
   }
   if (email == null) {
     throw new IllegalArgumentException("the buyer's email cannot be null");
   }
   if (password == null) {
     throw new IllegalArgumentException("buyer's password cannot be null");
   }

   String returnString = postCart(email, password, buyerInformation,
       encoder.generateParameters(cart, merchantKey),
       encoder.getBodyForCart(cart), ccOptions);

   log.trace("Leaving placeOrder");

   return returnString;
 }

  /** Places an order to gbuy, specifying the cart as an xml string.
  *
  * This function implements the buyer's actions necessary to place an order:
  *
  * 1- It posts the shopping cart to the gbuy url.
  *
  * 2- Logs in as a buyer.
  *
  * 3- If the initial address is specified, it posts it to the ajax url. This
  * triggers the merchant calculation callback for the predefined buyer
  * address.
  *
  * 4- It checks that the place order button is available.
  *
  * 5- It posts, one by one, the additional addresses or coupons/buy
  * certificates to the ajax url. This triggers the merchant calculation
  * callback in the server for the additional addresses, coupons and gift
  * certificates.
  *
  * 6- Places the order, posting the coupons and gift certificates found in
  * the additional addresses.
  *
  * @param cart The checkout cart to send. It cannot be null.
  *
  * @param email The buyer email used to log in to the GBuy service as a
  * buyer. Cannot be null.
  *
  * @param password The passoword used to log in to the GBuy service as a
  * buyer. Cannot be null.
  * @return returns the order number of the new posted cart order.
  */
 public String placeOrder(final String cart, final String email, final String
     password) {

   log.trace("Entering placeOrder");
   String returnString =  placeOrder(cart, email, password, null);
   log.trace("Leaving placeOrder");

   return returnString;
 }

  /** Places an order to gbuy, specifying the cart as an xml string.
   *
   * This function implements the buyer's actions necessary to place an order:
   *
   * 1- It posts the shopping cart to the gbuy url.
   *
   * 2- Logs in as a buyer.
   *
   * 3- If the initial address is specified, it posts it to the ajax url. This
   * triggers the merchant calculation callback for the predefined buyer
   * address.
   *
   * 4- It checks that the place order button is available.
   *
   * 5- It posts, one by one, the additional addresses or coupons/buy
   * certificates to the ajax url. This triggers the merchant calculation
   * callback in the server for the additional addresses, coupons and gift
   * certificates.
   *
   * 6- Places the order, posting the coupons and gift certificates found in
   * the additional addresses.
   *
   * @param cart The checkout cart to send. It cannot be null.
   *
   * @param email The buyer email used to log in to the GBuy service as a
   * buyer. Cannot be null.
   *
   * @param password The passoword used to log in to the GBuy service as a
   * buyer. Cannot be null.
   *
   * @param buyerInformation A list of either shipping addresses, coupons or
   *  gift certificates.
   * @return returns the order number of the new posted cart order.
   */
  public String placeOrder(final String cart, final String email, final String
      password, final List<BuyerInformation> buyerInformation) {
    return placeOrder(cart, email, password, buyerInformation, null);
  }

  /** Places an order to gbuy, specifying the cart as an xml string.
  *
  * This function implements the buyer's actions necessary to place an order:
  *
  * 1- It posts the shopping cart to the gbuy url.
  *
  * 2- Logs in as a buyer.
  *
  * 3- If the initial address is specified, it posts it to the ajax url. This
  * triggers the merchant calculation callback for the predefined buyer
  * address.
  *
  * 4- It checks that the place order button is available.
  *
  * 5- It posts, one by one, the additional addresses or coupons/buy
  * certificates to the ajax url. This triggers the merchant calculation
  * callback in the server for the additional addresses, coupons and gift
  * certificates.
  *
  * 6- Places the order, posting the coupons and gift certificates found in
  * the additional addresses.
  *
  * @param cart The checkout cart to send. It cannot be null.
  *
  * @param email The buyer email used to log in to the GBuy service as a
  * buyer. Cannot be null.
  *
  * @param password The passoword used to log in to the GBuy service as a
  * buyer. Cannot be null.
  *
  * @param buyerInformation A list of either shipping addresses, coupons or
  *  gift certificates.
  *
  * @param ccOptions the Carrier Calculated shipping options to verify.
  * @return returns the order number of the new posted cart order.
  */
 public String placeOrder(final String cart, final String email, final String
     password, final List<BuyerInformation> buyerInformation,
     final List<String> ccOptions) {

   log.trace("Entering placeOrder");

   if (cart == null) {
     throw new IllegalArgumentException("the cart cannot be null");
   }

   List<KeyValuePair> parameters =
     encoder.generateParameters(cart, merchantKey);

   String body = encoder.getBodyForCart(cart);

   String returnString = postCart(email, password, buyerInformation, parameters,
       body, ccOptions);

   log.trace("Leaving placeOrder");

   return returnString;
 }

  /** Places an order to gbuy.
   *
   * @param email The buyer email used to log in to the GBuy service as a
   * buyer. Cannot be null.
   *
   * @param password The passoword used to log in to the GBuy service as a
   * buyer. Cannot be null.
   *
   * @param buyerInformation A list of either shipping addresses, coupons or
   *  gift certificates.
   *
   * @param parameters The parameters that encode the content of the cart.
   *
   * @param body the body to be sent in the request.
   *
   * @param ccOptions The Carrier Calculated Shipping Options to verify.
   * @return returns the order number of the new posted cart order.
   */
  @SuppressWarnings("unchecked")
  private String postCart(final String email, final String password,
      final List <BuyerInformation> buyerInformation,
      final List <KeyValuePair> parameters, final String body,
      final List<String> ccOptions) {

    try {
      String path = post(parameters, body);
      startSeleniumOnPage(serverUrl + path);
      Thread.sleep(10000);
      loginAfterPlacingOrder(email, password);

      enterBuyerInformation(buyerInformation);

      // Check if the Carrier Calculated Shipping Options are shown.
      if (ccOptions != null && !ccOptions.isEmpty()) {
        seleniumRC.waitForCondition(
            "selenium.browserbot.getCurrentWindow()."
            + "document.getElementById('bottomBuyButton') != null", "30000");

        seleniumRC.waitForCondition(
            "selenium.browserbot.getCurrentWindow()."
            + "document.getElementById('bottomBuyButton')."
            + "disabled == false", "30000");

        // First make sure that the select drop-down for the methods is present.
        SeleniumUtil.waitForElementPresent(seleniumRC,
            "name=shippingMethodsViewName", (int) MC_TIMEOUT);
        // Then check if all elements are shown
        checkCarrierCalculatedOptions(seleniumRC, ccOptions);
      }

      return placeOrder();

      //TODO disable the comment when done.
//      seleniumRC.waitForCondition(
//          "selenium.browserbot.getCurrentWindow()." +
//          "document.getElementById('userLinks') != null", "3000");
//      seleniumRC.click("link=Sign out");
//      seleniumRC.close();

//      /* This code was added to handle the captcha issue on Google's internal
//       * servers.
//       */
//      log.debug("Comparing page title: (" + orderPage.getTitleText().trim()
//          + ") with (Google Accounts)");
//      if (orderPage.getTitleText().trim().equals("Google Accounts")) {
//        pass = (HtmlPasswordInput) form
//          .getInputByName("Passwd");
//        pass.setValueAttribute(password);
//
//        HtmlTextInput logincaptcha =
//          (HtmlTextInput) form.getInputByName("logincaptcha");
//        //TODO GET FROM ARGUMENTS.
//        logincaptcha.setValueAttribute("mackerel");
//
//        orderPage = (HtmlPage) form.submit();
//      }
    } catch (FailingHttpStatusCodeException e) {
      log.error("Error posting cart: " + e.getMessage());
      log.debug("Error content: " + e.getResponse().getContentAsString());
      throw new MalbecHttpErrorCodeException("Unable to place the order",
          e.getStatusCode(), e.getResponse().getContentAsString(), e);
    } catch (I18NDoesNotShipToAddressException e) {
      log.debug("Found I18N exception: " + e.getMessage());
      throw e;
    } catch (MalbecCarrierCalculatedShippingException e) {
      log.debug("Found Carrier Calculated Shipping exception: "
          + e.getMessage());
      throw e;
    } catch (Exception e) {
      log.error("Error posting cart: " + e.getMessage(), e);
      throw new RuntimeException(e);
    } finally {
      exit();
    }
  }

  /**
   * Starts the SeleniumRC.
   */
  private void startSeleniumRC() {
    log.debug("Starting Selenium RC...");
    seleniumRC.start();
    seleniumRCStarted = true;
    log.debug("Selenium RC started");
  }

  /**
   * Stops the SeleniumRC.
   */
  public void exit() {
    if (seleniumRCStarted) {
      log.debug("Stopping Selenium RC...");
      if (seleniumRCStarted) {
        seleniumRC.stop();
        seleniumRCStarted =  false;
        log.debug("Selenium RC stopped");
      } else {
        log.debug("Selenium RC was already stopped");
      }
    }
  }

  /**
   * Post the cart and open the buy page with selenium.
   * @param cart the Checkout Shopping cart to post. cannot be null.
   */
  public void postCart(final CheckoutShoppingCart cart) {
    try {
      String path = post(encoder.generateParameters(cart, merchantKey),
          encoder.getBodyForCart(cart));
      startSeleniumOnPage(serverUrl + path);      
    } catch (IOException e) {
      throw new RuntimeException("Could not post the cart to: " + serverUrl);
    }
  }

  /**
   * Posts a shopping cart and open the buy page with selenium.
   * @param cart the shopping cart to post as a string. Cannot be null or empty.
   */
  public void postCart(final String cart) {
    List<KeyValuePair> parameters = encoder.generateParameters(cart,
        merchantKey);
    try {
      String path = post(parameters, encoder.getBodyForCart(cart));
      startSeleniumOnPage(serverUrl + path);
    } catch (IOException e) {
      throw new RuntimeException("Could not post the cart to: " + serverUrl);
    }
  }

  /**
   * Logs the user in after the order has been placed. Selenium must be started
   * and the cart posted for this method to be called.
   * @param email the email of the buyer to login.
   * @param password the password of the buyer to login.
   */
  public void loginAfterPlacingOrder(
      final String email, final String password) {
    if (!seleniumRCStarted) {
      throw new RuntimeException("You must post the cart before logging in");
    }
    try {
      // Find the login frame
      seleniumRC.selectFrame("login");

      seleniumRC.type("Email", email);
      seleniumRC.type("Passwd", password);

      seleniumRC.click("null");
      seleniumRC.selectWindow("null");

      seleniumRC.waitForPageToLoad("30000");
    } catch (SeleniumException e) {
      log.error("The user cannot be logged in.", e);
      exit();
      throw new RuntimeException("The user cannot be logged in: "
          + e.getMessage(), e);
    }

    boolean badEmail =
      SeleniumUtil.waitForElementPresent(seleniumRC,
        "//*[@id='errormsg_0_Email']", BAD_LOGIN_TIMEOUT);
    boolean badLogin =
      SeleniumUtil.waitForElementPresent(seleniumRC,
        "//*[@id='errormsg_0_Passwd']", BAD_LOGIN_TIMEOUT);
    if (badEmail || badLogin) {
      exit();
      throw new RuntimeException(
          "Invalid login. Username and password do not match.");
    }
    //this forces to wait till the page is fully loaded.
    SeleniumUtil.waitForElementPresent(seleniumRC, "//*[@id='addressView']",
        30000);

    if (SeleniumUtil.waitForTextPresent(seleniumRC,
        "does not ship to this address", 5000)) {
      log.trace("Found that the Merchant does not ship to the buyer's "
          + "address. Throwing an exception.");
      throw new I18NDoesNotShipToAddressException("Merchant does not ship to"
          + " the buyer's address.");
    }
  }

  /**
   * Logs the user in into the user account front end, to see the purchase
   * history. Selenium must be started for this method to be called.
   * @param email the email of the buyer to login.
   * @param password the password of the buyer to login.
   */
  public void login(final String email, final String password) {
    log.debug("Entering login");
    if (!seleniumRCStarted) {
      log.warn("Selenium must be started to log in and is down. Starting...");
      startSeleniumRC();
    }
    try {
      seleniumRC.open(buyerURL);
      // Find the login frame

      seleniumRC.type("Email", email);
      seleniumRC.type("Passwd", password);

      seleniumRC.click("null");
      seleniumRC.selectWindow("null");

      seleniumRC.waitForPageToLoad("30000");

      boolean badEmail =
        SeleniumUtil.waitForElementPresent(seleniumRC,
            "//*[@id='errormsg_0_Email']", BAD_LOGIN_TIMEOUT);
      boolean badLogin =
        SeleniumUtil.waitForElementPresent(seleniumRC,
            "//*[@id='errormsg_0_Passwd']", BAD_LOGIN_TIMEOUT);
      if (badEmail || badLogin) {
        exit();
        throw new RuntimeException(
        "Invalid login. Username and password do not match.");
      }
      SeleniumUtil.waitForElementPresent(
          seleniumRC, "link=glob:*continue to use the service*", 3000);
      if (seleniumRC.getTitle().equals("Google Accounts")) {
        seleniumRC.click("link=glob:*continue to use the service*");
        seleniumRC.waitForPageToLoad("30000");
      }
      log.debug("User is logged in now");
      log.debug("Leaving login");
    } catch (SeleniumException e) {
      log.error("Error while accessing the user account: " + e.getMessage(), e);
      exit();
      throw new RuntimeException("Error while accessing the user account: "
          + e.getMessage(), e);
    }
  }

  /**
   * Gets the source code for the email message received for the new order.
   * @param email The email used to log in.
   * @param password the password used to log in.
   * @param orderNumber the order number for the expected email message.
   * @return the html source code for the email message.
   */
  public String getBuyerMessageMail(final String email, final String password,
      final String orderNumber) {
    DefaultSelenium gmailSeleniumRC = null;
    try {
      String gmailUrl = "http://gmail.google.com";
      gmailSeleniumRC = createSeleniumRc(gmailUrl);
      gmailSeleniumRC.start();
      gmailSeleniumRC.open(gmailUrl);
      gmailSeleniumRC.waitForPageToLoad("30000");

      //login
      gmailSeleniumRC.type("Email", email);
      gmailSeleniumRC.type("Passwd", password);
      gmailSeleniumRC.click("name=signIn");
      gmailSeleniumRC.waitForPageToLoad("30000");

      try {
        Thread.sleep(15000);
      } catch (InterruptedException e) {
        log.warn("Interrupted sleep", e);
      }

      gmailSeleniumRC.selectFrame("main");
      gmailSeleniumRC.selectFrame("v1");
      gmailSeleniumRC.waitForCondition(
          "selenium.browserbot.getCurrentWindow()"
          + ".document.getElementsByName('q')[0].value='" + orderNumber + "'",
      "1000");
      gmailSeleniumRC.submit("id=s");

      //open result if any
      try {
        Thread.sleep(5000);
      } catch (InterruptedException e) {
        log.warn("Interrupted sleep", e);
      }

      gmailSeleniumRC.keyPress("xpath=//body", "o");
      String result = gmailSeleniumRC.getHtmlSource();
      return result;
    } finally {
      if (gmailSeleniumRC != null) {
        gmailSeleniumRC.stop();
      }
    }
  }

  /**
   * Gets the order page for the given order number. You must call login first.
   * @param orderNumber the order number.
   * @return the order page.
   */
  public String getBuyerOrderInformation(final String orderNumber) {
    log.debug("Entering getBuyerOrderInformation");
    if (!seleniumRCStarted) {
      throw new RuntimeException("The selenium must be started to get the order"
          + " information");
    }
    try {
      String orderPage = buyerURL.endsWith("/") ? buyerURL : buyerURL + "/";
      orderPage += "view/receipt";

      Map<String, String> parameters = new HashMap<String, String>();
      parameters.put("t", orderNumber);
      log.debug("Leaving getBuyerOrderInformation");
      return getPage(orderPage, parameters);
    } catch (SeleniumException e) {
      log.error("Found an error while getting the order information from the"
          + " buyer page: " + e.getMessage(), e);
      exit();
      throw new RuntimeException("Could not get the order information: "
          + e.getMessage(), e);
    }
  }

  /**
   * Starts selenium on the buy page using the given path.
   * @param path the path to access the buy page.
   */
  private void startSeleniumOnPage(final String path) {
    startSeleniumRC();
    try {
      seleniumRC.setTimeout("5000");
      seleniumRC.open(path);
    } catch (Exception e) {
      //HACK
    }
    seleniumRC.setTimeout("30000");
  }

  /**
   * Gets the html source obtained when requesting an url with a set of
   * parameters.
   * @param url the url to request.
   * @param parameters the parameters to be posted.
   * @return the html source of the obtained page.
   */
  private String getPage(
      final String url, final Map<String, String> parameters) {
    String result = url;
    if (parameters.keySet().isEmpty()) {
      seleniumRC.open(result);
      return seleniumRC.getHtmlSource();
    }
    result += "?";
    for (Iterator<String> it = parameters.keySet().iterator(); it.hasNext();) {
      String key = it.next();
      result += key + "=" + parameters.get(key);
      result += "&";
    }
    result = result.substring(0, result.length() - 1);
    seleniumRC.open(result);
    return seleniumRC.getHtmlSource();
  }

  /**
   * Enters the list of buyerInformation objects given, such as coupons or
   * address changes, for example.
   * @param buyerInformation the list of BuyerInformation objects.
   */
  public void enterBuyerInformation(
      final List<BuyerInformation> buyerInformation) {
    try {
      //  wait for the initial mc if there is any.
      Thread.sleep(MC_TIMEOUT);

      // Enter addresses, coupons and gift certificates.
      if (buyerInformation != null) {
        BuyerVisitor buyerVisitor = new SeleniumBuyerVisitor(seleniumRC);
        for (BuyerInformation information : buyerInformation) {
          information.execute(buyerVisitor);
          Thread.sleep(MC_TIMEOUT);
        }
      }
    } catch (InterruptedException e) {
      throw new RuntimeException("Error entering buyer information", e);
    }
  }

  /**
   * Place an order. Click on the buy button. Should be called once the selenium
   * was started and the user has logged in.
   * @return the order number of the placed order.
   */
  public String placeOrder() {
    try {
      if (!seleniumRCStarted) {
        throw new RuntimeException("Selenium must be started before placing"
            + " the order");
      }
      // Check that the button exists on the page.
      try {
        seleniumRC.waitForCondition(
            "selenium.browserbot.getCurrentWindow()."
            + "document.getElementById('bottomBuyButton') != null", "30000");
      } catch (SeleniumException e) {
        log.error("The button is not visible on the current page. "
            + "Check that the user is logged in before placing the order", e);
        exit();
        throw new RuntimeException("Must be logged in to place an order", e);
      }

      // Check that the button is available to be clicked.
      try {
        seleniumRC.waitForCondition(
            "selenium.browserbot.getCurrentWindow()."
            + "document.getElementById('bottomBuyButton')."
            + "disabled == false", "30000");
      } catch (SeleniumException e) {
        log.error("The button to place order is disabled.", e);
        exit();
        throw new RuntimeException("Could not place the order: "
            + "button is disabled", e);
      }

      // Even if the bottomBuyButton is enabled just wait 2 seconds (looks like
      // a messy hack to emulate the human interaction with the UI) this is
      // done because when entering coupons the bottomBuyButton is enabled
      // before the coupon timeout expired.
      Thread.sleep(2000);

      // Place the order
      log.debug("Placing order");
      seleniumRC.click("bottomBuyButton");

      seleniumRC.waitForCondition(
          "selenium.getTitle() == 'Purchase Complete!'", "45000");

      return getOrderNumber(seleniumRC.getLocation());
    } catch (InterruptedException e) {
      exit();
      throw new RuntimeException("Error placing the order", e);
    }
  }

  /**
   * Get the current page html.
   * @return the current page html.
   */
  public String getPage() {
    if (!seleniumRCStarted) {
      throw new RuntimeException(
          "Cannot obtain a page when selenium is stopped");
    }
    try {
      return seleniumRC.getHtmlSource();
    } catch (SeleniumException e) {
      log.error("The current page could not be obtained: " + e.getMessage(),
          e);
      exit();
      throw new RuntimeException("Could not obtain page: " + e.getMessage(),
          e);
    }
  }

  /**
   * Get the buyer receipt as html. Should be called once the order was placed.
   * @return the buyer receipt as html.
   */
  public String getBuyerReceipt() {
    try {
      Thread.sleep(3000);
      SeleniumUtil.waitForTextPresent(
          seleniumRC, "Get up-to-date order progress", 3000);
      seleniumRC.click("link=Get up-to-date order progress");
      return seleniumRC.getHtmlSource();
    } catch (InterruptedException e) {
      throw new RuntimeException("Error trying to get the buyer receipt", e);
    }
  }

  /**
   * Gets the order number based on a url, the order number comes as a value of
   *  the t get parameter.
   * @param url The url that contains the order number.
   * @return The order number.
   */
  private String getOrderNumber(final String url) {
    log.trace("getting ordernumber from " + url);
    int position = url.indexOf("t=");
    if (position != -1) {
      String orderNumber = url.substring(position + 2);
      int ampIndex = url.indexOf("&");
      boolean moreParameters = (ampIndex != -1); 
      if (moreParameters) {
        orderNumber = url.substring(position + 2, ampIndex);        
      }
      return orderNumber;
    } else {
      throw new RuntimeException("The order number couldnt be found in the url"
          + " after placing the order");
    }
  }

  /**
   * Checks if all the given carrier calculated shipping options are shown in
   * the place order page.
   * @param selenium the {@link Selenium} class to use.
   * @param options the shipping option names to check.
   */
  private void checkCarrierCalculatedOptions(final Selenium selenium,
      final List<String> options) {
    List<String> foundOptions = Arrays
      .asList(selenium.getSelectOptions("name=shippingMethodsViewName"));
    for (String current : options) {
      if (!anyStartWith(foundOptions, current + " (")) {
        throw new MalbecCarrierCalculatedShippingException("Carrier calculated"
            + "shipping option not found: " + current);
      }
    }
  }

  /**
   * Checks if any of the strings in the container starts with the given prefix.
   * @param container The container to iterate.
   * @param prefix the prefix to be found.
   * @return true if at least one element is found.
   */
  private boolean anyStartWith(final List<String> container,
      final String prefix) {
    for (String current : container) {
      if (current.startsWith(prefix)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Posts the shopping cart and get the response.
   * @param parameters The parameters that encode the content of the cart
   * @param body the Request body to be sent.
   * @return Returns the path to the place order page.
   * @throws IOException when the URL is invalid, or any IO error occurs.
   */
  private String post(final List<KeyValuePair> parameters, final String body)
  throws IOException {
    if (body != null) {
      return postUnsigned(parameters, body);
    } else {
      return postSigned(parameters);
    }
  }

  /**
   * Posts the signed shopping cart and get the response.
   * @param parameters The parameters that encode the content of the cart
   * @return Returns the path to the place order page.
   * @throws IOException when the URL is invalid, or any IO error occurs.
   */
  private String postSigned(final List<KeyValuePair> parameters)
  throws IOException {
    WebClient client = createWebClient();

    log.debug("Posting shopping cart to " + postURL);
    WebRequestSettings request = new WebRequestSettings(new URL(postURL),
        SubmitMethod.POST);   
    request.setRequestParameters(parameters);    
    log.debug(parameters.toString());
    try {
      Page page = client.getPage(request);      
      String path = page.getWebResponse().getUrl().getFile();
      return path;
    } catch (Exception e) {
      //throw new RuntimeException("Could not connect to " + postURL);
      throw new RuntimeException(e);
    }
  }

  /**
   * Posts the shopping cart and get the response.
   * @param parameters The parameters that encode the content of the cart
   * @param body the Request body to be sent.
   * @return Returns the path to the place order page.
   * @throws IOException when the URL is invalid, or an IO error occurs.
   */
  @SuppressWarnings("unchecked")
  private String postUnsigned(final List<KeyValuePair> parameters,
      final String body)
  throws IOException {
    WebClient client = createWebClient();

    log.debug("Posting shopping cart to " + postURL);
    WebRequestSettings request = new WebRequestSettings(new URL(postURL),
        SubmitMethod.POST);

    request.setRequestBody(body);
    Map headers = new HashMap<String, String>();
    for (KeyValuePair current : parameters) {
      headers.put(current.getKey(), current.getValue());
    }
    request.setAdditionalHeaders(headers);
    try {
      Page page = client.getPage(request);      
      CheckoutRedirect response = (CheckoutRedirect) CartUtils.unmarshal(
          page.getWebResponse().getContentAsString()).getValue();
      URL url = new URL(URLDecoder.decode(response.getRedirectUrl(), "UTF-8"));
      String path = url.getFile();
      return path;
    } catch (Exception e) {
      throw new RuntimeException("Could not connect to " + postURL);
    }
  }

  /** Creates the WebClient that will be used by this BuyerRobot.
   *
   * @return The web client.
   */
  private WebClient createWebClient() {
    WebClient client = new WebClient();
    CookieWebConnection cookieWebConnection = new CookieWebConnection(client);
    client.setWebConnection(cookieWebConnection);
    client.setJavaScriptEnabled(false);
    return client;
  }

  /** An object that handles the actual communication portion of page
   * retrieval/submission.
   *
   * This connection sets the cookie policy as BROWSER_COMPATIBILITY.
   *
   * BROWSER_COMPATIBILITY: compatible with the common cookie management
   * practices (even if they are not 100% standards compliant)
   */
  private static class CookieWebConnection extends HttpWebConnection {

    /** Creates a CookieWebConnection.
     *
     * @param webClient The web client.
     */
    public CookieWebConnection(final WebClient webClient) {
      super(webClient);
    }

    /** Creates the httpClient that will be used by this CookieWebConnection.
     *
     * @return the client
     */
    protected HttpClient createHttpClient() {
      HttpClientParams httpClientParams = new HttpClientParams();
      httpClientParams.setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
      HttpClient httpClient = new HttpClient(httpClientParams);
      return httpClient;
    }
  }

  /** Creates a sample html page suitable to show in a browser and manually
   * post the shopping cart.
   *
   * @param cart The shopping cart to be posted.
   *
   * @return Returns a string with an html page that can be used to manually
   * post the order.
   */
  public String createHtmlPageForOrder(final CheckoutShoppingCart cart) {
    StringBuffer page = new StringBuffer();
    page.append("<html>\n");
    page.append("<body>\n");
    page.append(" <form method='post' action='").append(postURL).append("'>\n");

    List<KeyValuePair> parameters =
      encoder.generateParameters(cart, merchantKey);

    for (Iterator it = parameters.iterator(); it.hasNext();) {
      KeyValuePair pair = (KeyValuePair) it.next();

      page.append("  <input type='hidden' name='").append(pair.getKey()).
      append("' value='").append(pair.getValue()).append("'>\n");
    }

    page.append("  Post the cart:\n");
    page.append("  <input type='submit' name='Post'>\n");

    page.append(" </form>\n");
    page.append("</body>\n");
    page.append("</html>\n");

    return page.toString();
  }

  /**
   * Click the anchor that has the text received as parameter.
   * @param linkText The anchor text.
   * @param timeout the timeout to wait once link was clicked.
   */
  public void linkText(final String linkText, final long timeout) {
    SeleniumUtil.waitForElementPresent(seleniumRC, "link=" + linkText, 3000);
    seleniumRC.click("link=" + linkText);
    try {
      Thread.sleep(timeout);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  /**
   * Click the anchor that has the id attribute received as parameter.
   * @param linkId The anchor id attribute.
   * @param timeout the timeout to wait once link was clicked.
   */
  public void linkId(final String linkId, final long timeout) {
    SeleniumUtil.waitForElementPresent(seleniumRC, "id=" + linkId, 3000);
    seleniumRC.click("id=" + linkId);
    try {
      Thread.sleep(timeout);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  /**
   * Gets the text of an element. This works for any element that contains text.
   * @param elementId The element id attribute.
   * @param timeoutMillis The timeout in milliseconds to wait to get the element
   *  text.
   * @return The text of the element.
   */
  public String getElementContent(final String elementId,
      final int timeoutMillis) {
    SeleniumUtil.waitForElementPresent(
        seleniumRC, "id=" + elementId, timeoutMillis);
    return seleniumRC.getText("id=" + elementId);
  }
}

